import os
import xml.etree.ElementTree as ET

import numpy as np

from .easyofd.ofd import OFD


def traverse_dir(path, extension, files=None, files_name=None):
    if files_name is None:
        files_name = []
    if files is None:
        files = []
    for file in os.listdir(path):
        file_path = os.path.join(path, file)
        if os.path.isdir(file_path):
            traverse_dir(file_path, extension, files, files_name)
        else:
            tail = file_path.split('.')[-1]
            if tail in extension and not os.path.basename(file_path).startswith('~$'):
                files.append(file_path)
                files_name.append(os.path.basename(file_path))
    return zip(files, files_name)


def get_doc_xml(unziped_path):
    # unziped_path下包含了OFD.xml文件，我们解析该文件获取Document.xml文件
    ofd_path = os.path.join(unziped_path, 'OFD.xml')
    tree = ET.parse(ofd_path)
    # 获取根元素
    root = tree.getroot()
    # OFD.xml文件中可能包含多个Doc_i文件夹，document_xml_path里面存放所有Doc_i/Document.xml的路径
    document_xml_path = []
    doc_i_path = []
    # 遍历所有元素
    for elem in root.iter():
        # DocRoot中存放Document.xml文件信息
        if 'DocRoot' in elem.tag:
            document_xml_path.append(os.path.join(unziped_path, elem.text))
            ly_dirs = elem.text.split('/')
            doc_i_parent = '/'.join(ly_dirs[0:-1])
            doc_i_path.append(os.path.join(unziped_path, doc_i_parent))
    return document_xml_path, doc_i_path


def get_attaches(document_xml_path, doc_i_path):
    page_path = []
    attach_path = set()
    for i, document_path in enumerate(document_xml_path):
        tree = ET.parse(document_path)
        root = tree.getroot()
        for elem in root.iter():
            if elem.attrib.__contains__('BaseLoc'):
                page_path.append(os.path.join(doc_i_path[i], elem.attrib['BaseLoc']))
            if 'Attachments' in elem.tag:
                attach_path.add(os.path.join(doc_i_path[i], elem.text))
    return page_path, attach_path


def get_attach_path(attach_map_path):
    attach_names = []
    attach_ids = []
    attach_files_dir = []
    for document_path in attach_map_path:
        attach_parent_path = document_path.replace(os.path.basename(document_path), '')
        tree = ET.parse(document_path)
        root = tree.getroot()
        for elem in root.iter():
            if elem.attrib.__contains__('Name'):
                attach_names.append(elem.attrib['Name'])
            else:
                if attach_names:
                    base_name = os.path.basename(elem.text)
                    attach_ids.append(base_name)
                    attach_files_dir.append(os.path.join(attach_parent_path, elem.text.replace(base_name, '')))
    attach_map_path = {}
    if len(attach_names) == len(attach_ids):
        for i in range(len(attach_names)):
            attach_map_path[attach_names[i]] = os.path.join(attach_files_dir[i], attach_ids[i])
    else:
        print('ofd的Attach文件重命名失败！')
    return attach_map_path


def process_exception(unziped_path):
    """
    :param unziped_path: 解压后的ofd文件夹
    :return: 获取ofd文字内容的xml路径
    获取路径：OFD.xml -> Doc_i/Document.xml -> Pages/Page_i/Content.xml
    """
    document_xml_path, doc_i_path = get_doc_xml(unziped_path)
    # 通过解析Document.xml来获取到page内容，page中包含了ofd文本内容的路径信息
    page_path, attach_path = get_attaches(document_xml_path, doc_i_path)
    # 获取 ofd中的附件信息（如果有的话），并根据映射关系重命名附件
    attach_map_path = get_attach_path(attach_path)
    # 解析page中的内容，获取ofd的文本相关的内容
    ofd_content = get_content_xml(page_path)
    return ofd_content


def get_content_xml(iter_files):
    content_ls = []
    for fid, file_path in enumerate(iter_files):
        tree = ET.parse(file_path)
        # 获取根元素
        root = tree.getroot()
        # 遍历所有元素
        for elem in root.iter():
            text = elem.text
            if text is not None and text.strip() and 'TextCode' in elem.tag:
                content_ls.append(text.strip())
    content = ' '.join(content_ls).strip()
    return content


def merge_adjacent_items(items, y_tol=1.0, x_gap_tol=2.0):
    """
    合并“紧挨着”的项。

    参数：
      items：列表，每项为 (text, [x0, y0, width, height])，
             列表顺序表示阅读顺序（按行排列，每行中从左向右）。
      y_tol：用于判断两项是否在同一行的垂直容忍度，
             如果两项的 y 坐标差小于该值，则认为它们在同一行。
      x_gap_tol：判断两项在水平方向是否连续，
             如果后一项的 x0 与前一项右边界（x0+width）之间的距离小于该值，则认为紧挨着。

    返回：
      合并后的新列表，每项格式同原始项 (merged_text, [x0, y0, width, height])。
    """

    def merge_boxes(box1, box2):
        # box1, box2 格式均为 [x0, y0, width, height]
        x0 = min(box1[0], box2[0])
        y0 = min(box1[1], box2[1])
        # 计算右下角位置
        x1 = max(box1[0] + box1[2], box2[0] + box2[2])
        y1 = max(box1[1] + box1[3], box2[1] + box2[3])
        return [x0, y0, x1 - x0, y1 - y0]

    merged = []
    if not items:
        return merged

    # 初始化第一个合并项（初始合并组）
    current_text, current_box = items[0]

    for text, box in items[1:]:
        # 判断是否在同一行：比较y0值的差异是否小于 y_tol
        if abs(box[1] - current_box[1]) <= y_tol:
            # 判断两项是否紧挨着：计算当前项右侧 x 坐标与下一个项左侧 x 坐标之差
            current_right = current_box[0] + current_box[2]
            gap = box[0] - current_right
            if gap < 0:
                # 若出现重叠，则视为连续
                gap = 0
            if gap <= x_gap_tol:
                # 合并文本（这里直接连接，可根据需求增加空格等）
                current_text += text
                # 更新合并后的边界框
                current_box = merge_boxes(current_box, box)
                continue

        # 若不满足合并条件，将当前合并项存入结果，再开始新的合并项
        merged.append((current_text, current_box))
        current_text = text
        current_box = box

    # 别忘了加入最后一个合并组
    merged.append((current_text, current_box))
    return merged


def estimate_tolerances(items):
    """
    通过列表项的位置数据估计最佳的 y_tol 和 x_gap_tol 参数值。

    参数：
      items: 列表，每个元素为 (text, [x0, y0, width, height])
             列表顺序表示阅读顺序（按行排列，每行中从左向右）。

    返回：
      (y_tol, x_gap_tol) 的元组：
        y_tol：两个项在垂直方向上差值小于该值时，认为在同一行。
        x_gap_tol：两个项在同一行时，后一项的 x0 与前项右侧边界之间的间隔小于该值则认为是“紧挨着”。
    """
    # 如果列表为空则返回默认容忍值
    if not items:
        return 1.0, 1.0

    # 先统计所有文本项的高度
    heights = np.array([bbox[3] for text, bbox in items])
    median_height = np.median(heights)

    # 估算 y_tol：
    # 对于列表中“相邻”的两个项，计算它们 y0 的差值，
    # 并挑选出那些小于 median_height 的差值认为是可能在同一行的候选。
    y_diffs = []
    for i in range(1, len(items)):
        # 取两项的 y0（这里也可以用 y0+height/2 来近似中心）
        y_prev = items[i - 1][1][1]
        y_curr = items[i][1][1]
        diff = abs(y_curr - y_prev)
        y_diffs.append(diff)
    y_diffs = np.array(y_diffs)

    # 仅关注那些较小的差值（认为是行内的）
    same_row_diffs = y_diffs[y_diffs <= median_height]
    if same_row_diffs.size:
        # 取 90 百分位，外加一个很小的余量
        candidate_y_tol = np.percentile(same_row_diffs, 90) + 1e-3
    else:
        # 万一没有小差值，则默认取中位数的一半
        candidate_y_tol = median_height * 0.5
    y_tol = candidate_y_tol

    # 估算 x_gap_tol：
    # 对于那些认为属于同一行（根据 y_tol 判断）的相邻项，
    # 计算后一项的 x0 与前项右侧边界之间的差值。
    gaps = []
    for i in range(1, len(items)):
        # 如果 y 坐标差小于 y_tol，则认为这两项在同一行
        if abs(items[i][1][1] - items[i - 1][1][1]) < y_tol:
            x_prev, width_prev = items[i - 1][1][0], items[i - 1][1][2]
            x_curr = items[i][1][0]
            current_right = x_prev + width_prev
            gap = x_curr - current_right
            # 如果 gap 小于 0（文本框有重叠或紧贴），则认为 gap 为 0
            gap = max(gap, 0)
            gaps.append(gap)
    if gaps:
        gaps = np.array(gaps)
        # 取 90 百分位，再加微小余量
        candidate_x_gap_tol = np.percentile(gaps, 90) + 1e-3
    else:
        candidate_x_gap_tol = 3.0  # 若无法计算 gap，则给个默认值
    x_gap_tol = candidate_x_gap_tol

    return y_tol, x_gap_tol


class MyOFD(OFD):
    def __init__(self, unzip_path):
        super().__init__(unzip_path)
        self.read()  # 读取ofdb64
        document_xml_path, doc_i_path = get_doc_xml(unzip_path)
        page_path, attach_path = get_attaches(document_xml_path, doc_i_path)
        # 获取 ofd中的附件信息（如果有的话），并根据映射关系重命名附件
        self.attach_map_path = get_attach_path(attach_path)

    def get_content(self):
        page_content = []
        for doc_i, file in enumerate(self.data):
            page_info = file['page_info']
            for key, value in page_info.items():
                items = []
                for item in value['text_list']:
                    # page_content.append((doc_i, key + 1, item['pos'], item['text']))
                    items.append((item['text'], item['pos']))
                y_tol, x_gap_tol = estimate_tolerances(items)
                merged_items = merge_adjacent_items(items, y_tol, x_gap_tol)
                page_content.extend([(doc_i, key + 1, pos, text) for (text, pos) in merged_items])
                pass
        return page_content
